﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Security.Cryptography;
using EnvDTE;
using NBusiness.CodeDom.Services;
using VSLangProj;

namespace NBusiness.VisualStudio
{
    internal class ESharpReflectionService : IReflectionService
    {
        Project _project;
        Dictionary<string, List<TypeData>> _cached = new Dictionary<string, List<TypeData>>();

        public ESharpReflectionService(Project project)
        {
            _project = project;
        }

        public void Refresh()
        {
            _cached = null;
        }

        #region ITypeFinder Members

        #region Find Assignable From

        public TypeData[] FindAssignableFrom(TypeData baseType)
        {
            List<TypeData> results = new List<TypeData>();
            try
            {
                if (_project != null)
                {
                    TypeData[] assignableTypes = FindAssignableTypes(
                        baseType,
                        _project.CodeModel.CodeElements);
                    
                    results.AddRange(assignableTypes);

                    Type type = Type.GetType(baseType.FullName + ", " + baseType.AssemblyName);
                    VSProject vsProj = _project.Object as VSProject;
                    if (type != null && vsProj != null)
                    {
                        foreach (Reference reference in vsProj.References)
                        {
                            if (reference.SourceProject == null && reference.CopyLocal)
                            {
                                Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a =>
                                    !(a is AssemblyBuilder) && a.Location == reference.Path);

                                if (assembly != null)
                                {
                                    var types = from t in assembly.GetTypes()
                                                where
                                                    type.IsAssignableFrom(t) &&
                                                    !t.IsInterface &&
                                                    !t.IsAbstract
                                                select new TypeData(t);

                                    results.AddRange(types);
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex);
            }
            return results.ToArray();
        }

        private TypeData[] FindAssignableTypes(TypeData @base, CodeElements elements)
        {
            List<TypeData> results = new List<TypeData>();

            foreach (CodeElement element in elements)
            {
                if (element.IsCodeType &&
                    (element.Kind == vsCMElement.vsCMElementClass ||
                    element.Kind == vsCMElement.vsCMElementStruct))
                {
                    CodeType codeType = element as CodeType;
                    CodeElements members = codeType.Members;

                    bool isAbstract = false;
                    CodeClass cc = codeType as CodeClass;
                    if (cc != null)
                        isAbstract = cc.IsAbstract;

                    CodeStruct cs = codeType as CodeStruct;
                    if (cs != null)
                        isAbstract = cs.IsAbstract;

                    if (!isAbstract && IsAssignableFrom(codeType, @base))
                    {
                        TypeData td = CreateType(codeType);
                        if (td != null)
                            results.Add(td);
                    }
                }
                else if (
                    element.Kind == vsCMElement.vsCMElementNamespace &&
                    element.Name != "MS" &&
                    element.Name != "Microsoft" &&
                    element.Name != "System")
                {
                    CodeNamespace cns = element as CodeNamespace;

                    CodeElements members = cns.Members;

                    TypeData[] types = FindAssignableTypes(@base, members);
                    if (types.Length > 0)
                        results.AddRange(types);
                }
            }

            return results.ToArray();
        }

        #endregion

        #region Find Callable As

        public MethodData[] FindCallableAs(MethodData delegateSignature)
        {
            List<MethodData> results = new List<MethodData>();

            if (_project != null)
            {
                MethodData[] assignableTypes = FindCallableMethods(
                    delegateSignature,
                    _project.CodeModel.CodeElements);
                
                results.AddRange(assignableTypes);

                Type type = Type.GetType(delegateSignature.FullName + ", " + delegateSignature.DeclaringType.AssemblyName);
                VSProject vsProj = _project.Object as VSProject;
                if (type != null && vsProj != null)
                {
                    foreach (Reference reference in vsProj.References)
                    {
                        if (reference.SourceProject == null && reference.CopyLocal)
                        {
                            Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a =>
                                !(a is AssemblyBuilder) && a.Location == reference.Path);

                            if (assembly != null)
                            {
                                var methods = from t in assembly.GetTypes()
                                              from m in t.GetMethods(BindingFlags.Static | BindingFlags.Public)
                                              where IsCallable(m, delegateSignature)
                                              select new MethodData(m);

                                results.AddRange(methods);
                            }
                        }
                    }
                }
            }

            return results.ToArray();
        }

        private MethodData[] FindCallableMethods(MethodData @base, CodeElements elements)
        {
            List<MethodData> results = new List<MethodData>();

            foreach (CodeElement element in elements)
            {
                if (!element.IsCodeType && 
                    element.Kind == vsCMElement.vsCMElementFunction)
                {
                    CodeFunction function = element as CodeFunction;
                    if (IsCallable(function, @base))
                    {
                        Dictionary<string, TypeData> parameters = new Dictionary<string, TypeData>();
                        foreach (CodeParameter parameter in function.Parameters)
                            parameters.Add(parameter.Name, CreateType(parameter.Type.CodeType));

                        MethodData method = new MethodData
                        {
                            Name = function.Name,
                            FullName = function.FullName,
                            DeclaringType = CreateType(function.Parent as CodeType),
                            ReturnType = null,
                            Signature = parameters
                        };
                        results.Add(method);
                    }
                }
                else if (
                    (element.Kind == vsCMElement.vsCMElementNamespace ||
                    element.Kind == vsCMElement.vsCMElementClass ||
                    element.Kind == vsCMElement.vsCMElementStruct ) &&
                    element.Name != "MS" &&
                    element.Name != "Microsoft" &&
                    element.Name != "System")
                {
                    CodeElements members = null;
                    CodeNamespace cns = element as CodeNamespace;
                    if (cns != null)
                        members = cns.Members;
                    
                    CodeClass cc = element as CodeClass;
                    if (cc != null)
                        members = cc.Members;

                    CodeStruct cs = element as CodeStruct;
                    if (cs != null)
                        members = cs.Members;

                    if(members!=null)
                    {
                        MethodData[] types = FindCallableMethods(@base, members);
                        if (types.Length > 0)
                            results.AddRange(types);
                    }
                }
            }

            return results.ToArray();
        }

        private bool IsCallable(CodeFunction function, MethodData @base)
        {
            if (function.IsShared && 
                function.Access == vsCMAccess.vsCMAccessPublic &&
                @base.Signature != null &&
                function.Parameters.Count == @base.Signature.Count)
            {
                //for (int x = 0; x < @base.Signature.Count; x++)
                //{
                //    CodeParameter parameter = (CodeParameter)function.Parameters.Item(x + 1);

                //    if (parameter.Type.TypeKind == vsCMTypeRef.vsCMTypeRefOther ||
                //        parameter.Type.TypeKind == vsCMTypeRef.vsCMTypeRefArray ||
                //        !IsAssignableFrom(parameter.Type.CodeType, @base.Signature[x]))
                //        return false;
                //}
                int x = 1;
                foreach(string key in @base.Signature.Keys)
                {
                    CodeParameter parameter = (CodeParameter)function.Parameters.Item(x++);

                    if (parameter.Type.TypeKind == vsCMTypeRef.vsCMTypeRefOther ||
                        parameter.Type.TypeKind == vsCMTypeRef.vsCMTypeRefArray ||
                        !IsAssignableFrom(parameter.Type.CodeType, @base.Signature[key]))
                        return false;
                }
                return true;
            }

            return false;
        }

        private bool IsCallable(MethodInfo method, MethodData delegateType)
        {
            ParameterInfo[] parameters = method.GetParameters();
            if (parameters.Length != delegateType.Signature.Count)
                return false;

            if (method.ReturnType != delegateType.ReturnType)
                return false;

            int x = 0;
            foreach(string key in delegateType.Signature.Keys)
            {
                Type p1 = parameters[x++].ParameterType;
                TypeData p2 = delegateType.Signature[key];

                if (p1 == p2)
                    continue;

                Type b = p1.BaseType;
                bool match = false;
                while (b != null)
                {
                    if (b == p2)
                    {
                        match = true;
                        break;
                    }
                    b = b.BaseType;
                }
                if (match)
                    continue;

                foreach (Type i in p1.GetInterfaces())
                {
                    if (i == p2)
                        continue;
                }

                return false;
            }

            return true;
        }

        #endregion

        #endregion

        #region Helpers

        private bool IsAssignableFrom(CodeType codeType, TypeData @base)
        {
            if (codeType.FullName == @base.FullName)
                return true;

            foreach (object obj in codeType.Bases)
            {
                CodeType baseType = obj as CodeType;
                if (baseType != null)
                {
                    if (baseType.FullName == @base.FullName)
                        return true;

                    if (IsAssignableFrom(baseType, @base))
                        return true;
                }
            }

            switch (codeType.Kind)
            {
                case vsCMElement.vsCMElementClass:
                    CodeClass codeClass = codeType as CodeClass;
                    foreach (CodeInterface implementedInterface in codeClass.ImplementedInterfaces)
                    {
                        if (implementedInterface.FullName == @base.FullName ||
                            IsAssignableFrom(implementedInterface as CodeType, @base))
                            return true;
                    }
                    break;
                case vsCMElement.vsCMElementStruct:
                    break;
                case vsCMElement.vsCMElementInterface:
                    break;
            }

            return false;
        }

        public TypeData CreateType(CodeType codeType)
        {
            if (codeType.InfoLocation == vsCMInfoLocation.vsCMInfoLocationProject)
            {
                string fullname = codeType.FullName;
                string assembly = null;
                string version = null;
                byte[] publicKeyToken = null;
                CultureInfo cultureInfo = null;
                string path = null;

                assembly = (string)codeType.ProjectItem.ContainingProject.Properties.Item("AssemblyName").Value;
                version = (string)codeType.ProjectItem.ContainingProject.Properties.Item("AssemblyVersion").Value;

                try
                {
                    bool signAssembly = (bool)codeType.ProjectItem.ContainingProject.Properties.Item("SignAssembly").Value;
                    if (signAssembly)
                    {
                        string keyfile = (string)codeType.ProjectItem.ContainingProject.Properties.Item("AssemblyOriginatorKeyFile").Value;
                        string localPath = (string)codeType.ProjectItem.ContainingProject.Properties.Item("LocalPath").Value;
                        keyfile = Path.Combine(localPath, keyfile);

                        FileStream publicKeyStream = File.Open(keyfile, FileMode.Open);
                        byte[] publicPrivateKey = new byte[publicKeyStream.Length];
                        publicKeyStream.Read(publicPrivateKey, 0, (int)publicKeyStream.Length);

                        StrongNameKeyPair pair = new StrongNameKeyPair(publicPrivateKey);
                        SHA1 sha = SHA1.Create();
                        byte[] hash = sha.ComputeHash(pair.PublicKey);

                        publicKeyToken = new byte[8];
                        Array.Copy(hash, hash.Length - publicKeyToken.Length, publicKeyToken, 0, publicKeyToken.Length);
                        Array.Reverse(publicKeyToken, 0, publicKeyToken.Length);
                    }
                }
                // this may happen if the snk is opened by another process. Just use the
                // type and assembly name.
                catch (IOException)
                {
                    version = null;
                    publicKeyToken = null;
                    cultureInfo = null;
                }

                path = (string)codeType.ProjectItem.ContainingProject.Properties.Item("FullPath").Value;
                path = Path.Combine(path, (string)codeType.ProjectItem.ContainingProject.ConfigurationManager.ActiveConfiguration.Properties.Item("OutputPath").Value);
                path = Path.Combine(path, (string)codeType.ProjectItem.ContainingProject.Properties.Item("OutputFileName").Value);

                return new TypeData
                {
                    FullName = fullname,
                    AssemblyName = assembly,
                    Version = version,
                    PublicKeyToken = publicKeyToken,
                    Culture = cultureInfo,
                    Path = path
                };
            }

            return null;
        } 

        #endregion
    }

    
}
